// Copyright 2010 ESRI
// 
// All rights reserved under the copyright laws of the United States
// and applicable international laws, treaties, and conventions.
// 
// You may freely redistribute and use this sample code, with or
// without modification, provided you include the original copyright
// notice and use restrictions.
// 
// See the use restrictions at &lt;your ArcGIS install location&gt;/DeveloperKit10.0/userestrictions.txt.
// 

using System;
using System.Collections.Generic;
using System.Text;
using System.ComponentModel;
using ESRI.ArcGIS.Geodatabase;
using ESRI.ArcGIS.Geometry;

namespace Akiha.Core
{
  /// <summary>
  /// This class provides a PropertyDescriptor for a single field of an IRow
  /// </summary>
  /// <remarks>
  /// This class can be used by an ITypedList implementation to provide a property
  /// description for a single field in an ITable.
  /// </remarks>
  internal class FieldPropertyDescriptor: PropertyDescriptor
  {
    #region Private Members
    /// <summary>
    /// Store the index of the IField that this property descriptor describes
    /// </summary>
    private int wrappedFieldIndex;
    
    /// <summary>
    /// Store the .NET type of the value stored in the IField this property
    /// represents
    /// </summary>
    private Type netType;

    /// <summary>
    /// This is used to store the actual .NET type of a field that uses a CV
    /// domain. It retains the type allowing as to restore it when the UseCVDomain
    /// property is false;
    /// </summary>
    private Type actualType;
    
    /// <summary>
    /// Store the esri type of the value stored in the IField this property
    /// represents
    /// </summary>
    private esriFieldType esriType;
    
    /// <summary>
    /// Indicates whether this field is editable or not.
    /// </summary>
    /// <remarks>
    /// This will determined by looking at the Editable property of the IField
    /// and the type of the field. We currently don't support the editing of
    /// blob or geometry fields.
    /// </remarks>
    bool isEditable = true;

    /// <summary>
    /// Used to start and stop editing when adding/updating/deleting rows
    /// </summary>
    private IWorkspaceEdit wkspcEdit;

    /// <summary>
    /// The coded value domain for the field this instance represents, if any
    /// </summary>
    private ICodedValueDomain cvDomain;

    /// <summary>
    /// This will be true if we are currently using the string values for the
    /// coded value domain and false if we are using the numeric values.
    /// </summary>
    private bool useCVDomain;

    /// <summary>
    /// This type converter is used when the field this instance represents has
    /// a coded value domain and we are displaying the actual domain values
    /// </summary>
    private TypeConverter actualValueConverter;

    /// <summary>
    /// This type converter is used when the field this instance represents has
    /// a coded value domain and we are displaying the names of the domain values
    /// </summary>
    private TypeConverter cvDomainValDescriptionConverter;
    #endregion Private Members

    #region Construction/Destruction
    /// <summary>
    /// Initializes a new instance of the <see cref="FieldPropertyDescriptor"/> class.
    /// </summary>
    /// <param name="wrappedTable">The wrapped table.</param>
    /// <param name="fieldName">Name of the field within wrappedTable.</param>
    /// <param name="fieldIndex">Index of the field within wrappedTable.</param>
    public FieldPropertyDescriptor(ITable wrappedTable, string fieldName, int fieldIndex)
      : base(fieldName, null)
    {
      wrappedFieldIndex = fieldIndex;

      // Get the field this property will represent. We will use it to
      // get the field type and determine whether it can be edited or not. In
      // this case, editable means the field's editable property is true and it
      // is not a blob, geometry or raster field.
      IField wrappedField = wrappedTable.Fields.get_Field(fieldIndex);
      esriType = wrappedField.Type;
      isEditable = wrappedField.Editable && 
        (esriType != esriFieldType.esriFieldTypeBlob) &&
        (esriType != esriFieldType.esriFieldTypeRaster) &&
        (esriType != esriFieldType.esriFieldTypeGeometry);
      netType = actualType = EsriFieldTypeToSystemType(wrappedField);
      wkspcEdit = ((IDataset)wrappedTable).Workspace as IWorkspaceEdit;
    } 
    #endregion Construction/Destruction

    /// <summary>
    /// Gets a value indicating whether the field represented by this property 
    /// has a CV domain.
    /// </summary>
    /// <value>
    /// 	<c>true</c> if this instance has a CV domain; otherwise, <c>false</c>.
    /// </value>
    public bool HasCVDomain
    {
      get
      {
        return null != cvDomain;
      }
    }

    /// <summary>
    /// Sets a value indicating whether [use CV domain].
    /// </summary>
    /// <value><c>true</c> if [use CV domain]; otherwise, <c>false</c>.</value>
    public bool UseCVDomain
    {
      set
      {
        useCVDomain = value;
        if (value)
        {
          // We want the property type for this field to be string
          netType = typeof(string);
        }
        else
        {
          // Restore the original type
          netType = actualType;
        }
      }
    }

    #region Public Overrides
    /// <summary>
    /// Gets the type converter for this property.
    /// </summary>
    /// <remarks>
    /// We need to override this property as the base implementation sets the
    /// converter once and reuses it as required. We can't do this if the field
    /// this instance represents has a coded value domain and we change from
    /// using the value to using the name or vice versa. The reason for this is
    /// that if we are displaying the domain name, we need a string converter and
    /// if we are displaying the domain value, we will need one of the numeric
    /// converters.
    /// </remarks>
    /// <returns>A <see cref="T:System.ComponentModel.TypeConverter"></see> 
    /// that is used to convert the <see cref="T:System.Type"></see> of this 
    /// property.</returns>
    /// <PermissionSet><IPermission class="System.Security.Permissions.SecurityPermission, 
    /// mscorlib, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" 
    /// version="1" Flags="UnmanagedCode"/></PermissionSet>
    public override TypeConverter Converter
    {
      get
      {
        TypeConverter retVal = null;

        if (null != cvDomain)
        {
          if (useCVDomain)
          {
            if (null == cvDomainValDescriptionConverter)
            {
              // We want a string converter
              cvDomainValDescriptionConverter = TypeDescriptor.GetConverter(typeof(string));
            }

            retVal = cvDomainValDescriptionConverter;
          }
          else
          {
            if (null == actualValueConverter)
            {
              // We want a converter for the type of this field's actual value
              actualValueConverter = TypeDescriptor.GetConverter(actualType);
            }

            retVal = actualValueConverter;
          }
        }
        else
        {
          // This field doesn't have a coded value domain, the base implementation
          // works fine.
          retVal = base.Converter;
        }

        return retVal;
      }
    }

    /// <summary>
    /// Returns whether resetting an object changes its value.
    /// </summary>
    /// <param name="component">The component to test for reset capability.
    /// This will be an IRow</param>
    /// <returns>
    /// true if resetting the component changes its value; otherwise, false.
    /// </returns>
    public override bool CanResetValue(object component)
    {
      return false;
    }

    /// <summary>
    /// Gets the type of the component this property is bound to.
    /// </summary>
    /// <value></value>
    /// <returns>A <see cref="T:System.Type"></see> that represents the type of 
    /// component this property is bound to. When the 
    /// <see cref="M:System.ComponentModel.PropertyDescriptor.GetValue(System.Object)"></see> 
    /// or <see cref="M:System.ComponentModel.PropertyDescriptor.SetValue(System.Object,System.Object)"></see> 
    /// methods are invoked, the object specified might be an instance of this type.</returns>
    public override Type ComponentType
    {
      get { return typeof(IRow); }
    }

    /// <summary>
    /// Gets the current value of the property on a component.
    /// </summary>
    /// <param name="component">The component (an IRow) with the property for 
    /// which to retrieve the value.</param>
    /// <remarks>
    /// This will return the field value for all fields apart from geometry, raster and Blobs.
    /// These fields will return the string equivalent of the geometry type.
    /// </remarks>
    /// <returns>
    /// The value of a property for a given component. This will be the value of
    /// the field this class instance represents in the IRow passed in the component
    /// parameter.
    /// </returns>
    public override object GetValue(object component)
    {
      object retVal = null;

      IRow givenRow = (IRow)component;
      try
      {
        // Get value
        object value = givenRow.get_Value(wrappedFieldIndex);

        if ((null != cvDomain) && useCVDomain)
        {
          value = cvDomain.get_Name(Convert.ToInt32(value));
        }

        switch (esriType)
        {
            case esriFieldType.esriFieldTypeBlob:
                retVal = "Blob";
                break;
            
            case esriFieldType.esriFieldTypeGeometry:
                retVal = GetGeometryTypeAsString(value);
                break;
            
            case esriFieldType.esriFieldTypeRaster:
                retVal = "Raster";
                break;
            
            default:
                retVal = value;
                break;
        }
      }
      catch (Exception e)
      {
        System.Diagnostics.Debug.WriteLine(e.Message);
      }

      return retVal;
    }

    /// <summary>
    /// Gets a value indicating whether this property is read-only or not.
    /// </summary>
    /// <value></value>
    /// <returns>true if the property is read-only; otherwise, false.</returns>
    public override bool IsReadOnly
    {
      get { return !isEditable; }
    }

    /// <summary>
    /// Gets the type of the property.
    /// </summary>
    /// <value></value>
    /// <returns>A <see cref="T:System.Type"></see> that represents the type 
    /// of the property.</returns>
    public override Type PropertyType
    {
      get { return netType; }
    }

    /// <summary>
    /// Resets the value for this property of the component to the default value.
    /// </summary>
    /// <param name="component">The component (an IRow) with the property value 
    /// that is to be reset to the default value.</param>
    public override void ResetValue(object component)
    {

    }

    /// <summary>
    /// Sets the value of the component to a different value.
    /// </summary>
    /// <remarks>
    /// If the field this instance represents does not have a coded value domain,
    /// this method simply sets the given value and stores the row within an edit
    /// operation. If the field does have a coded value domain, the method first
    /// needs to check that the given value is valid. If we are displaying the 
    /// coded values, the value passed to this method will be a string and we will
    /// need to see if it is one of the names in the cv domain. If we are not
    /// displaying the coded values, we will still need to check that the given
    /// value is within the domain. If the value is not within the domain, an
    /// error will be displayed and the method will return.
    /// Note that the string comparison is case sensitive.
    /// </remarks>
    /// <param name="component">The component (an IRow) with the property value 
    /// that is to be set.</param>
    /// <param name="value">The new value.</param>
    public override void SetValue(object component, object value)
    {
      IRow givenRow = (IRow)component;

      if (null != cvDomain)
      {
        // This field has a coded value domain
        if (!useCVDomain)
        {
          // Check value is valid member of the domain
          if (!((IDomain)cvDomain).MemberOf(value))
          {
            System.Windows.Forms.MessageBox.Show(string.Format(
              "Value {0} is not valid for coded value domain {1}", value.ToString(), ((IDomain)cvDomain).Name));
            return;
          }
        }
        else
        {
          // We need to convert the string value to one of the cv domain values
          // Loop through all the values until we, hopefully, find a match
          bool foundMatch = false;
          for (int valueCount = 0; valueCount < cvDomain.CodeCount; valueCount++)
          {
            if (value.ToString() == cvDomain.get_Name(valueCount))
            {
              foundMatch = true;
              value = valueCount;
              break;
            }
          }

          // Did we find a match?
          if (!foundMatch)
          {
            System.Windows.Forms.MessageBox.Show(string.Format(
              "Value {0} is not valid for coded value domain {1}", value.ToString(), ((IDomain)cvDomain).Name));
            return;
          }
        }
      }
      givenRow.set_Value(wrappedFieldIndex, value);

      // Start editing if we aren't already editing
      bool weStartedEditing = false;
      if (!wkspcEdit.IsBeingEdited())
      {
        wkspcEdit.StartEditing(false);
        weStartedEditing = true;
      }

      // Store change in an edit operation
      wkspcEdit.StartEditOperation();
      givenRow.Store();
      wkspcEdit.StopEditOperation();

      // Stop editing if we started here
      if (weStartedEditing)
      {
        wkspcEdit.StopEditing(true);
      }

    }

    /// <summary>
    /// When overridden in a derived class, determines a value indicating whether 
    /// the value of this property needs to be persisted.
    /// </summary>
    /// <param name="component">The component (an IRow) with the property to be examined for 
    /// persistence.</param>
    /// <returns>
    /// true if the property should be persisted; otherwise, false.
    /// </returns>
    public override bool ShouldSerializeValue(object component)
    {
      return false;
    } 
    #endregion Public Overrides

    #region Private Methods
    /// <summary>
    /// Converts the specified ESRI field type to a .NET type.
    /// </summary>
    /// <param name="esriType">The ESRI field type to be converted.</param>
    /// <returns>The appropriate .NET type.</returns>
    private Type EsriFieldTypeToSystemType(IField field)
    {
      esriFieldType esriType = field.Type;

      // Does this field have a domain?
      cvDomain = field.Domain as ICodedValueDomain;
      if ((null != cvDomain) && useCVDomain)
      {
        return typeof(string);
      }

      try
      {
        switch (esriType)
        {
          case esriFieldType.esriFieldTypeBlob:
            //beyond scope of sample to deal with blob fields
            return typeof(string);
          case esriFieldType.esriFieldTypeDate:
            return typeof(DateTime);
          case esriFieldType.esriFieldTypeDouble:
            return typeof(double);
          case esriFieldType.esriFieldTypeGeometry:
            return typeof(string);
          case esriFieldType.esriFieldTypeGlobalID:
            return typeof(string);
          case esriFieldType.esriFieldTypeGUID:
            return typeof(Guid);
          case esriFieldType.esriFieldTypeInteger:
            return typeof(Int32);
          case esriFieldType.esriFieldTypeOID:
            return typeof(Int32);
          case esriFieldType.esriFieldTypeRaster:
            //beyond scope of sample to correctly display rasters
            return typeof(string);
          case esriFieldType.esriFieldTypeSingle:
            return typeof(Single);
          case esriFieldType.esriFieldTypeSmallInteger:
            return typeof(Int16);
          case esriFieldType.esriFieldTypeString:
            return typeof(string);
          default:
            return typeof(string);
        }
      }
      catch (Exception ex)
      {
        System.Diagnostics.Debug.WriteLine(ex.Message);
        return typeof(string);
      }
    }

    /// <summary>
    /// Gets the geometry type as string.
    /// </summary>
    /// <param name="value">The value.</param>
    /// <returns>The string equivalent of the geometry type</returns>
    private string GetGeometryTypeAsString(object value)
    {
      string retVal = "";
      IGeometry geometry = value as IGeometry;
      if (geometry != null)
      {
        retVal = geometry.GeometryType.ToString();
      }
      return retVal;
    }
    #endregion Private Methods
  }
}
